Skip to content
47 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The phone app's scaffolding, in apps/mobile: Expo SDK 57 with Expo Router, Android first, with Chat, Code and Customize. It signs in with g1t's own OAuth 2.1 (PKCE, a client id that encodes "g1t for Android" and g1t://oauth/callback, so nothing is registered), keeps the server and credentials in the phone's keystore, refreshes the 30-day token a minute early and shares the single-use refresh between requests. A personal access token is the other way in, and a self-hosted g1t the other server.1import { CUSTOMIZE_SECTIONS, type CustomizeSection } from "@g1t/contracts/customize";
2import { Redirect, Stack, useLocalSearchParams } from "expo-router";
3import * as WebBrowser from "expo-web-browser";
4import { ExternalLink } from "lucide-react-native";
5import { SectionList, View } from "react-native";
6
7import { Badge, Button, Row, Screen, SectionHeader, Text } from "@/components/ui";
8import { CUSTOMIZE, type CustomizeItem, customizeItems, customizePath } from "@/lib/customize";
9import { siteUrl } from "@/lib/server";
10import { useSignedIn } from "@/lib/session";
11import { space } from "@/theme";
12
13/** One part of Customize: its listings, grouped, each opening where it is set up. */
14export default function CustomizeSectionScreen() {
15 const { section } = useLocalSearchParams<{ section: string }>();
16 const { workspace, server } = useSignedIn();
17 if (!CUSTOMIZE_SECTIONS.includes(section as CustomizeSection)) return <Redirect href="/customize" />;
18 const id = section as CustomizeSection;
19 const slug = workspace?.slug ?? "";
20 const items = customizeItems(id, slug);
21 const groups = [...new Set(items.map((item) => item.group))].map((group) => ({ title: group, data: items.filter((item) => item.group === group) }));
22 const open = (path: string) => WebBrowser.openBrowserAsync(siteUrl(server, path));
23
24 return (
25 <Screen>
26 <Stack.Screen options={{ title: CUSTOMIZE[id].title }} />
27 <SectionList
28 sections={groups}
29 keyExtractor={(item) => item.id}
30 stickySectionHeadersEnabled={false}
31 ListHeaderComponent={
32 <View style={{ padding: space.lg, gap: space.md }}>
33 <Text tone="muted">{CUSTOMIZE[id].description}</Text>
34 <Button title={`Manage ${CUSTOMIZE[id].title.toLowerCase()} on g1t`} variant="secondary" icon={ExternalLink} onPress={() => open(customizePath(id, slug))} />
35 </View>
36 }
37 renderSectionHeader={({ section: group }) => <SectionHeader title={group.title} />}
38 renderItem={({ item }) => <Item item={item} onPress={() => open(item.sitePath)} />}
39 />
40 </Screen>
41 );
42}
43
44function Item({ item, onPress }: { item: CustomizeItem; onPress: () => void }) {
45 const badge = item.status === "available" ? <Badge label="Available" tone="success" /> : item.status === "soon" ? <Badge label="Soon" /> : <Badge label="Built in" tone="accent" />;
46 return <Row title={item.name} subtitle={item.description} trailing={badge} onPress={item.status === "soon" ? undefined : onPress} />;
47}